/*
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/.
* Copyright (c) 2013, MPL CodeInside http://codeinside.ru
*/
package ru.codeinside.gws.core.cproto;
import com.sun.xml.ws.developer.SchemaValidationFeature;
import com.sun.xml.ws.dump.MessageDumpingFeature;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.xml.sax.SAXException;
import ru.codeinside.gws.api.ClientLog;
import ru.codeinside.gws.api.ClientProtocol;
import ru.codeinside.gws.api.ClientRequest;
import ru.codeinside.gws.api.ClientResponse;
import ru.codeinside.gws.api.CryptoProvider;
import ru.codeinside.gws.api.Enclosure;
import ru.codeinside.gws.api.Packet;
import ru.codeinside.gws.api.Revision;
import ru.codeinside.gws.api.RouterPacket;
import ru.codeinside.gws.api.ServiceDefinition;
import ru.codeinside.gws.api.ServiceDefinitionParser;
import ru.codeinside.gws.api.VerifyResult;
import ru.codeinside.gws.api.XmlNormalizer;
import ru.codeinside.gws.api.XmlSignatureInjector;
import ru.codeinside.gws.core.Xml;
import javax.xml.namespace.QName;
import javax.xml.soap.MessageFactory;
import javax.xml.soap.MimeHeaders;
import javax.xml.soap.SOAPBody;
import javax.xml.soap.SOAPBodyElement;
import javax.xml.soap.SOAPEnvelope;
import javax.xml.soap.SOAPException;
import javax.xml.soap.SOAPMessage;
import javax.xml.soap.SOAPPart;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamSource;
import javax.xml.validation.Schema;
import javax.xml.validation.SchemaFactory;
import javax.xml.ws.BindingProvider;
import javax.xml.ws.Dispatch;
import javax.xml.ws.Service;
import javax.xml.ws.WebServiceException;
import javax.xml.ws.WebServiceFeature;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URL;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
/**
* В тестах можно включить дамп: HttpTransportPipe.dump = true;
*/
public class ClientProtocolImpl implements ClientProtocol {
//
static boolean validate = false;
static boolean validateBaseSchema = false;
static boolean dumping = false;
//
private final ServiceDefinitionParser definitionParser;
private final CryptoProvider cryptoProvider;
private final XmlNormalizer xmlNormalizer;
private final XmlSignatureInjector injector;
private final Map<URL, ServiceDefinition> definitionMap = new HashMap<URL, ServiceDefinition>();
private final Logger logger = Logger.getLogger(getClass().getName());
private final String REV;
private final Revision revisionNumber;
private final String xsdSchema;
transient private Schema schema;
public ClientProtocolImpl(Revision revision, String namespace, String xsdSchema,
ServiceDefinitionParser definitionParser, CryptoProvider cryptoProvider,
XmlNormalizer xmlNormalizer, XmlSignatureInjector injector) {
this.revisionNumber = revision;
this.REV = namespace;
this.xsdSchema = xsdSchema;
this.definitionParser = definitionParser;
this.cryptoProvider = cryptoProvider;
this.xmlNormalizer = xmlNormalizer;
this.injector = injector;
}
@Override
final public Revision getRevision() {
return revisionNumber;
}
@Override
final public ClientResponse send(URL wsdlUrl, ClientRequest request, ClientLog clientLog) {
try {
NormalizedRequest normalizedRequest = createNormalizedRequest(wsdlUrl, request, clientLog);
try {
SOAPMessage soapRequest;
if (request.requestMessage != null && request.requestMessage.length > 0) {
MessageFactory factory = MessageFactory.newInstance();
soapRequest = factory.createMessage(new MimeHeaders(), new ByteArrayInputStream(request.requestMessage));
if (soapRequest.getSOAPHeader().getFirstChild() == null) {
signSoapMessage(soapRequest);
}
} else {
soapRequest = buildSoapMessage(normalizedRequest);
signSoapMessage(soapRequest);
}
return getClientResponse(wsdlUrl, clientLog, normalizedRequest, soapRequest);
} catch (RuntimeException e) {
throw e;
}
} catch (RuntimeException e) {
logException(clientLog, e);
throw e;
} catch (Exception e) {
logException(clientLog, e);
throw new RuntimeException(e);
}
}
private void logException(ClientLog clientLog, Exception e) {
if (clientLog != null) {
clientLog.log(e);
}
}
private ClientResponse getClientResponse(URL wsdlUrl, ClientLog clientLog, NormalizedRequest normalizedRequest, SOAPMessage soapRequest) {
Dispatch<SOAPMessage> dispatch = createSoapMessageDispatch(wsdlUrl, normalizedRequest);
final Map<String, Object> ctx = dispatch.getRequestContext();
logger.finest("Use address '" + normalizedRequest.portSoapAddress + "' for " + normalizedRequest.action);
if (clientLog != null) {
ctx.put(ClientLog.class.getName(), clientLog);
}
ctx.put(BindingProvider.ENDPOINT_ADDRESS_PROPERTY, normalizedRequest.portSoapAddress);
final String soapAction = normalizedRequest.operation.soapAction;
if (soapAction != null) {
ctx.put(BindingProvider.SOAPACTION_USE_PROPERTY, true);
ctx.put(BindingProvider.SOAPACTION_URI_PROPERTY, soapAction);
}
ClientResponse clientResponse;
try {
clientResponse = processResult(dispatch.invoke(soapRequest));
} catch (SOAPException e) {
throw new RuntimeException(e);
} catch (WebServiceException e) {
logger.log(Level.WARNING, "GWS fail " + e.getLocalizedMessage());
Throwable cause = e.getCause();
while (cause instanceof RuntimeException) {
Throwable root = cause.getCause();
if (root == null) {
break;
}
cause = root;
}
if (cause instanceof IOException) {
throw new RuntimeException(cause);
}
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
}
throw e;
}
logResponse(clientLog, clientResponse);
return clientResponse;
}
private void logResponse(ClientLog clientLog, ClientResponse clientResponse) {
if (clientLog != null) {
clientLog.logResponse(clientResponse);
}
}
private SOAPMessage buildSoapMessage(NormalizedRequest normalizedRequest) throws Exception {
SOAPMessage soapRequest = createSoapMessage(normalizedRequest);
soapRequest.setProperty(SOAPMessage.CHARACTER_SET_ENCODING, "UTF-8");
return soapRequest;
}
/**
* Подготовить SOAP-сообщение перед отправкой
*
* @param wsdlUrl ссылка на описание сервиса в формате WSDL.
* @param request запрос от клиента к поствщику.
* @param log журнал клиента.
* @param normalizedSignedInfo нормализованный блок Body для получения подписи ОВ
* @return предварительное сообщение для отправки
*/
@Override
public SOAPMessage createMessage(URL wsdlUrl, ClientRequest request, ClientLog log, OutputStream normalizedSignedInfo) {
NormalizedRequest normalizedRequest = createNormalizedRequest(wsdlUrl, request, log);
try {
SOAPMessage soapMessage = buildSoapMessage(normalizedRequest);
//на случай, если в маршруте нет этапа подписи ЭП-ОВ
if (normalizedSignedInfo == null) {
return soapMessage;
}
ByteArrayOutputStream normalizedBody = new ByteArrayOutputStream();
xmlNormalizer.normalize(soapMessage.getSOAPBody(), normalizedBody);
ByteArrayInputStream normalizedBodyIS = new ByteArrayInputStream(normalizedBody.toByteArray());
byte[] normalizedBodyDigest = cryptoProvider.digest(normalizedBodyIS);
// TODO: захват тела ответа зависит от провайдера!
// Для Metro нужно переделывать "трубы" http://metro.java.net/guide/ch02.html#logging
// пример1 - http://musingsofaprogrammingaddict.blogspot.ru/2010/03/runtime-configuration-of-schema.html
// пример2 - http://marek.potociar.net/2009/10/19/custom-metro-tube-interceptor/
injector.prepareSoapMessage(soapMessage, normalizedBodyDigest);
Element signedInfo = (Element) soapMessage.getSOAPHeader().getFirstChild().getFirstChild().getFirstChild();
xmlNormalizer.normalize(signedInfo, normalizedSignedInfo);
return soapMessage;
} catch (Exception e) {
logException(log, e);
throw new RuntimeException(e);
}
}
private Dispatch<SOAPMessage> createSoapMessageDispatch(URL wsdlUrl, NormalizedRequest normalizedRequest) {
//TODO: кешиировать сервис по wsdl и имени?
Service service = Service.create(wsdlUrl, normalizedRequest.service);
// TODO: захват тела ответа зависит от провайдера!
// Для Metro нужно переделывать "трубы" http://metro.java.net/guide/ch02.html#logging
// пример1 - http://musingsofaprogrammingaddict.blogspot.ru/2010/03/runtime-configuration-of-schema.html
// пример2 - http://marek.potociar.net/2009/10/19/custom-metro-tube-interceptor/
final List<WebServiceFeature> features = new ArrayList<WebServiceFeature>();
if (validate) {
features.add(new SchemaValidationFeature());
}
if (dumping) {
features.add(new MessageDumpingFeature(ClientProtocolImpl.class.getName(), Level.INFO, false));
}
return service.createDispatch(
normalizedRequest.port,
SOAPMessage.class,
Service.Mode.MESSAGE,
features.toArray(new WebServiceFeature[features.size()])
);
}
private NormalizedRequest createNormalizedRequest(URL wsdlUrl, ClientRequest request, ClientLog clientLog) {
validateWsdlUrl(wsdlUrl);
validateClientRequest(request);
logRequest(request, clientLog);
final ServiceDefinition wsdl = parseAndCacheDefinition(wsdlUrl);
return normalize(wsdl, wsdlUrl, request);
}
private void logRequest(ClientRequest request, ClientLog clientLog) {
if (clientLog != null) {
clientLog.logRequest(request);
}
}
private void validateClientRequest(ClientRequest request) {
if (request == null) {
throw new IllegalArgumentException("request is null");
}
}
private void validateWsdlUrl(URL wsdlUrl) {
if (wsdlUrl == null) {
throw new IllegalArgumentException("wsdlUrl is null");
}
}
private ServiceDefinition parseAndCacheDefinition(URL wsdlUrl) {
ServiceDefinition definition;
synchronized (definitionMap) {
definition = definitionMap.get(wsdlUrl);
if (definition == null) {
definition = definitionParser.parseServiceDefinition(wsdlUrl);
if (definition == null || definition.services == null) {
throw new IllegalStateException(wsdlUrl.toString());
}
definitionMap.put(wsdlUrl, definition);
}
}
return definition;
}
private SOAPMessage createSoapMessage(final NormalizedRequest request) throws Exception {
final MessageFactory factory = MessageFactory.newInstance();
final SOAPMessage message = factory.createMessage();
final SOAPPart part = message.getSOAPPart();
final SOAPEnvelope envelope = part.getEnvelope();
// Стандартные пространства
envelope.addNamespaceDeclaration("smev", REV)//
.addNamespaceDeclaration("wsu",
"http://docs.oasis-open.org/wss/2004/01/oasis-200401-wss-wssecurity-utility-1.0.xsd");
final SOAPBody body = envelope.getBody();
body.addAttribute(envelope.createQName("Id", "wsu"), "body");// только для подписи системы
final QName inArg = request.operation.in.parts.values().iterator().next();
// TODO: может ли отличаться пространство вызова?
SOAPBodyElement action = body.addBodyElement(envelope.createName(inArg.getLocalPart(), "SOAP-WS", inArg.getNamespaceURI()));
Xml.fillSmevMessageByPacket(action, request.packet, revisionNumber);
Xml.addMessageData(request.appData, request.enclosureDescriptor, request.enclosures, action, part, cryptoProvider, revisionNumber);
validateBySchema(part);
return message;
}
private void signSoapMessage(SOAPMessage message) {
cryptoProvider.sign(message);
validateBySchema(message.getSOAPPart());
}
private ClientResponse processResult(final SOAPMessage message) throws SOAPException {
ClientResponse response = new ClientResponse();
// проверка на пакет СМЭВ
validateBySchema(message.getSOAPPart());
VerifyResult result = cryptoProvider.verify(message);
response.verifyResult = result;
if (result.error != null) {
// даже не пытаться разбирать сбойный пакет!
return response;
}
if (result.recipient == null) {
logger.fine("Сертификата посредника нет либо тестовый провайдер безопасности!");
}
if (result.actor == null) {
logger.fine("Сертификата нет либо тестовый провайдер безопасности!");
}
RouterPacket routerPacket = Xml.parseRouterPacket(message.getSOAPHeader(), revisionNumber);
if (routerPacket != null) {
if (routerPacket.direction != RouterPacket.Direction.RESPONSE) {
throw new IllegalStateException("Ошибка роутера СМЭВ: вернул запрос");
}
}
response.routerPacket = routerPacket;
final SOAPBody soapBody = message.getSOAPBody();
if ("Fault".equals(soapBody.getNodeName())
&& "http://www.w3.org/2003/05/soap-envelope".equals(soapBody.getNamespaceURI())) {
logger.warning("Не обработанная ошбка SOAP " + soapBody);
} else {
final Element action = Xml.parseAction(soapBody);
if (action == null) {
throw new IllegalStateException("Пустое тело пакета");
}
response.action = new QName(action.getNamespaceURI(), action.getLocalName());
response.packet = Xml.parseSmevMessage(action, revisionNumber);
final Xml.MessageDataContent mdc = Xml.processMessageData(message, action, revisionNumber, cryptoProvider);
response.enclosureDescriptor = mdc.requestCode;
response.appData = mdc.appData;
response.enclosures = mdc.attachmens != null ? mdc.attachmens.toArray(new Enclosure[mdc.attachmens.size()]) : null;
}
return response;
}
private void validateBySchema(Document document) {
if (validateBaseSchema) {
final Schema schema = getOrLoadSchema();
final long startMs = System.currentTimeMillis();
try {
schema.newValidator().validate(new DOMSource(document));
} catch (SAXException e) {
e.printStackTrace(System.out);
throw new RuntimeException(e);
} catch (IOException e) {
throw new RuntimeException(e);
}
System.out.println("VALIDATE SCHEMA: " + (System.currentTimeMillis() - startMs) + "ms");
}
}
private Schema getOrLoadSchema() {
if (schema == null) {
synchronized (this) {
if (schema == null) {
final long startMs = System.currentTimeMillis();
final SchemaFactory factory = SchemaFactory.newInstance("http://www.w3.org/2001/XMLSchema");
factory.setResourceResolver(new W3cResourceResolver("schema/"));
InputStream is = getClass().getClassLoader().getResourceAsStream(xsdSchema);
if (is == null) {
throw new IllegalStateException();
}
try {
schema = factory.newSchema(new StreamSource(is));
} catch (SAXException e) {
throw new RuntimeException(e);
}
System.out.println("LOAD SCHEMA: " + (System.currentTimeMillis() - startMs) + "ms");
}
}
}
return schema;
}
private NormalizedRequest normalize(ServiceDefinition wsdl, URL wsdlUrl, ClientRequest request) {
if (wsdl.services == null) {
throw new IllegalArgumentException("Invalid wsdl " + wsdlUrl);
}
if (!wsdl.namespaces.contains(REV)) {
throw new IllegalArgumentException("WSDL " + wsdlUrl + " not use " + REV);
}
QName serviceName = request.service;
if (request.service == null) {
if (wsdl.services.size() != 1) {
throw new IllegalArgumentException("Ambiguous service in " + wsdlUrl);
}
serviceName = wsdl.services.keySet().iterator().next();
}
ServiceDefinition.Service serviceDef = wsdl.services.get(serviceName);
if (serviceDef == null || serviceDef.ports == null) {
throw new IllegalArgumentException("Invalid service " + serviceName);
}
QName portName = request.port;
if (portName == null) {
if (serviceDef.ports.size() != 1) {
throw new IllegalArgumentException("Ambiguous port for service " + serviceName);
}
portName = serviceDef.ports.keySet().iterator().next();
}
ServiceDefinition.Port port = serviceDef.ports.get(portName);
if (port == null || port.operations == null) {
throw new IllegalArgumentException("Invalid port " + portName + " in service " + serviceName);
}
String portSoapAddress = request.portAddress;
if (portSoapAddress == null) {
portSoapAddress = port.soapAddress;
}
if (portSoapAddress == null) {
throw new IllegalArgumentException("Missed soapAddress for port " + portName + " in service " + serviceName);
}
QName action = request.action;
if (action == null) {
if (port.operations.size() != 1) {
throw new IllegalArgumentException("Ambiguous operation for port " + portName + " in service " + serviceName);
}
action = port.operations.keySet().iterator().next();
}
ServiceDefinition.Operation operation = port.operations.get(action);
if (operation == null || operation.in == null || operation.out == null) {
throw new IllegalArgumentException("Invalid operation " + action + " for port " + portName + " in service " + serviceName);
}
if (operation.in.parts == null || operation.in.parts.size() != 1) {
throw new IllegalArgumentException("Invalid parts operation " + action + " for port " + portName + " in service " + serviceName);
}
if (operation.out.parts == null || operation.out.parts.size() != 1) {
throw new IllegalArgumentException("Invalid parts operation " + action + " for port " + portName + " in service " + serviceName);
}
final NormalizedRequest normalized = new NormalizedRequest();
normalized.packet = request.packet;
normalized.action = action;
normalized.service = serviceName;
normalized.port = portName;
normalized.portSoapAddress = portSoapAddress;
normalized.appData = request.appData;
normalized.enclosureDescriptor = request.enclosureDescriptor;
normalized.enclosures = request.enclosures;
normalized.applicantSign = request.applicantSign;
normalized.operation = operation;
return normalized;
}
final static class NormalizedRequest {
public Packet packet;
public QName action;
public QName service;
public QName port;
public String portSoapAddress;
public String appData;
public String enclosureDescriptor;
public Enclosure[] enclosures;
public boolean applicantSign;
ServiceDefinition.Operation operation;
}
}